#include "nutshell.h"
#include "Alias.h"
#include <vector>
#include <string>
#include <iostream>
#include <fstream>
#include <ios>
#include "Set.h"
#include "Cd.h"
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <stdexcept>

using std::vector;
using std::map;
using std::string;
using std::cin;
using std::cout;
using std::endl;
using std::runtime_error;

using namespace nutshell;

int main()
{
	// create a Nutshell and run it
	Nutshell nut;
	return nut.start();
}

// Called to initialize and run the shell
int Nutshell::start()
{
	// set up the built-ins
	built_ins["set"] = &mySet;
	built_ins["alias"] = &myAlias;
	built_ins["unalias"] = &myAlias;
	built_ins["cd"] = &myCd;
	
	// run commands from the .nutshell file
	// the .nutshell file should be in the HOME directory
	std::ifstream in;
	in.open(".nutshell");
		if (!in) cout << ".nutshell file not found in HOME directory." << endl;
	while (in) 
	{
		// read each line from the file
		// (a command must be one a single line)
		string line;
		getline(in, line);
		
		// tokenize the command line
		vector<string> v = Scanner::scan_command(line);
		
		if (v.empty()) // if the command is blank, don't do anything more
			continue;
			
		// parse the commands into a vector of Command pointers
		vector<Command*> cmds;
		try
		{
			cmds = parseCommands( v );
		}
		catch (runtime_error e)
		{
			cout << "Error: " << e.what() << endl;
			continue;
		}
	
		// feed the vector of Command pointers to the command runner to be run
		try
		{
			runCommands( cmds );
		}
		catch (runtime_error e)
		{
			cout << "Error: " << e.what() << endl;
		}
		
		// Delete the Command objects
		for (vector<Command*>::iterator itr = cmds.begin(); itr != cmds.end(); ++itr)
			delete *itr;
	}
	in.close();

	// MAIN SHELL LOOP
	// steps:
	// 1. scan the command line input into tokens
	// 2. parse the tokens into a vector of Command pointers
	// 3. feed the vector of Command pointers to the command runner
	// 4. DELETE THE COMMAND OBJECTS!!!
	while(true)
	{
		
	
		// output the prompt
		cout << mySet.get_prompt() << std::flush;
		
		// scan the command line input into tokens
		vector<string> v = Scanner::scanCommands();
	
		if (v.empty()) // if the command is blank, do nothing more
			continue;
		else if (v[0] == "bye") // the user wants to quit
			break;
			
		// parse tokens into a vector of Command pointers
		vector<Command*> cmds;
		try
		{
			cmds = parseCommands( v );
		}
		catch (runtime_error e)
		{
			cout << "Error: " << e.what() << endl;
			continue;
		}
	
		// pass the vector of Command pointers to the command runner
		try
		{
			runCommands( cmds );
		}
		catch (runtime_error e)
		{
			cout << "Error: " << e.what() << endl;
		}
		
		// Delete the Command objects
		for (vector<Command*>::iterator itr = cmds.begin(); itr != cmds.end(); ++itr)
			delete *itr;
	}
	
	// not really any cleanup to do, just return
	return 0;
}

// COMMAND RUNNER
// accepts a vector of Command pointers
// if there is only a single, built-in command, it simply calls the built-in function
// if there is only a single command, forks and executes the command
// if there are more than one command, forks, pipes, and executes the commands
void Nutshell::runCommands( vector<Command*>& cmds )
{
	// if there is only a single, built-in command, just call the function
	if (cmds.size() == 1 && built_ins.find(cmds[0]->command_name) != built_ins.end() )
	{
		built_ins[cmds[0]->command_name]->do_command(cmds[0]->argc, cmds[0]->argv);
	}
	// otherwise
	else
	{
		/* fork child process */
		pid_t childpid = fork();

		if (childpid == -1)
		{
			/* Unable to fork */
			throw runtime_error("Unable to fork.");
		}
		else if (childpid == 0)
		{
			/* child process */
			try
			{
				// call auxilary function to do the actual command running
				runCommands_aux( cmds );
			}
			catch (runtime_error e)
			{
				cout << "Error: " << e.what() << endl;
			}
			exit(0);
		}
	
		// wait for the LAST command to complete
		wait(NULL);
	}
}

// auxillary command running function
// forks, pipes, and runs commands
void Nutshell::runCommands_aux( vector<Command*>& cmds )
{
	// get the PATH, to be used to find commands
	vector<string>& path = mySet.get_path();
		
	// before running commands, make sure they exist
	for (vector<Command*>::iterator citr = cmds.begin(); citr != cmds.end(); ++citr)
	{
		// is it a built-in command?
		if ( built_ins.find( (*citr)->command_name ) != built_ins.end() )
			continue;
	
		// is it in the current directory?
		int cmd_runnable = access( (*citr)->command_name.c_str(), X_OK );
	
		// is it in the PATH?
		if ( cmd_runnable < 0)
		{
			for (vector<string>::const_iterator pitr = path.begin(); pitr != path.end(); ++pitr)
			{
				string full_path = *pitr + "/" + (*citr)->command_name;
				cmd_runnable = access( full_path.c_str(), X_OK );
				if (cmd_runnable >= 0)
				{
					(*citr)->command_name = full_path;
					break;
				}
			}
		
			// could not find the command
			if (cmd_runnable < 0)
				throw runtime_error((*citr)->command_name + " is not a valid command.");
		}
		
	}

	// create the needed pipes
	int pipefd[cmds.size()-1][2];
	int i = 0;
	for (; i < cmds.size()-1; ++i)
	{
		int result = pipe( pipefd[i] );

		if(result < 0)
			throw runtime_error("Unable to create a pipe.");
	}

	// for all but the last command
	for (i = 0; i < cmds.size()-1; ++i)
	{
		/* fork child process */
		pid_t childpid = fork();

		if (childpid == -1)
		{
			/* Unable to fork */
			throw runtime_error("Unable to fork.");
		}
		else if (childpid == 0)
		{
			/* child process */

			/* close unneeded pipe connections */
			int j;
			for ( j = 0; j < cmds.size()-1; j++)
			{
				if ( j != i )
					close( pipefd[j][1] );
				if ( j != i - 1 )
					close( pipefd[j][0] );
			}

			// duplicate read end of pipe to stdin, or
			// if first command, duplicate input redirect file to stdin
			if ( i > 0 )
				dup2( pipefd[i - 1][0], STDIN_FILENO );
			else
				dup2( cmds[i]->in, STDIN_FILENO );

			/* duplicate write end of pipe to stdout */
			dup2( pipefd[i][1], STDOUT_FILENO);

			// if a built-in command, just call the command function
			if ( built_ins.find(cmds[i]->command_name) != built_ins.end() )
			{
				built_ins[cmds[i]->command_name]->do_command(cmds[i]->argc, cmds[i]->argv);
			}
			else
			{ 
				// if a normal command, exec the command
				
				/* execute the command */
				execv( cmds[i]->command_name.c_str(), cmds[i]->argv );
			
				/* never should get here */
				throw runtime_error(cmds[i]->command_name + " was not able to run.");
			}
			
			return;
		}
	}
	
	/* set up pipe and execute the last command */

	/* close unneeded pipe connections */
	for (int j = 0; j < i; j++)
	{
		close( pipefd[j][1] );

		if ( j != i - 1 )
			close( pipefd[j][0] );
	}

	// duplicate read end of pipe to stdin, or
	// if first command, duplicate input redirect file to stdin
	if ( i > 0 )
		dup2( pipefd[i - 1][0], STDIN_FILENO );
	else
		dup2( cmds[i]->in, STDIN_FILENO );
		
	// duplicate output redirect file to stdout
	dup2( cmds[i]->out, STDOUT_FILENO );

	/* execute the last command */
	
	// if built-in command, just call the command function
	if ( built_ins.find(cmds[i]->command_name) != built_ins.end() )
	{
		built_ins[cmds[i]->command_name]->do_command(cmds[i]->argc, cmds[i]->argv);
	}
	else
	{
		// if normal command, exec
		
		/* execute the command */
		execv( cmds[i]->command_name.c_str(), cmds[i]->argv );
	
		/* never should get here */
		throw runtime_error(cmds[i]->command_name + " was not able to run.");
	}
}

// COMMAND PARSER
// accepts a vector of command tokens
// and outputs a vector of Command pointers
// ready to be run.
vector<Command*> Nutshell::parseCommands( vector<string> cmd_str )
{
	// first, do aliasing and ~ completion
	for (vector<string>::size_type i = 0; i < cmd_str.size(); ++i)
	{
		// if the token is a command name and an alias
		if ( (i == 0 || cmd_str[i-1] == "|") && myAlias.is_alias(cmd_str[i]) )
		{
			// expand the alias
			vector<string> replacement = myAlias.expand_alias( cmd_str[i] );
			cmd_str.erase(cmd_str.begin() + i);
			cmd_str.insert(cmd_str.begin() + i, replacement.begin(), replacement.end() );
		}
		
		// if the token starts with a ~, try to expand it
		if ( !cmd_str[i].empty() && cmd_str[i][0] == '~' )
			cmd_str[i] = myCd.tilde_expansion( cmd_str[i] );
	}

	vector<Command*> cmds;
	
	// file descriptors for input and output redirection
	int in_fd = STDIN_FILENO;
	int out_fd = STDOUT_FILENO;
	
	// parse in/out redirects
	int lt_index = 0;
	int gt_index = 0;
	for (int i = 0; i < cmd_str.size(); ++i)
	{
		// if an input redirect token is found
		if (cmd_str[i] == "<")
		{
			if (lt_index == 0) // is it the only one?
				lt_index = i;
			else
				throw runtime_error("Only one input redirection is allowed.");
				
		}
		// is an output redirect token is found
		else if (cmd_str[i] == ">")
		{
			if (gt_index == 0) // is it the only one?
				gt_index = i;
			else
				throw runtime_error("Only one output redirection is allowed.");
		}
	}
	
	if (gt_index > 0) //if an output redirect was provided
	{
		if (gt_index < lt_index)
			throw runtime_error("Input redirection must appear before output redirection.");
		else if (gt_index < cmd_str.size() - 2)
			throw runtime_error("Output redirection must only appear at end of command line.");
		else if (gt_index == cmd_str.size() - 1)
			throw runtime_error("No file provided for output redirection.");
			
		// get the PATH to search for redirect file
		vector<string>& path = mySet.get_path();
		
		// is the file in the current directory?
		out_fd = open( cmd_str[gt_index + 1].c_str(), O_WRONLY | O_CREAT, S_IRWXU);
		
		// is the file in the PATH?
		if ( out_fd < 0)
		{
			for (vector<string>::const_iterator itr = path.begin(); itr != path.end(); ++itr)
			{
				string full_path = *itr + "/" + cmd_str[gt_index + 1];
				out_fd = open( full_path.c_str(), O_WRONLY | O_CREAT, S_IRWXU );
				if (out_fd >= 0)
					break;
			}
			
			// output redirect file was not found
			if (out_fd < 0)
				throw runtime_error("Cannot find file for output redirection.");
		}
			
		// remove the tokens for output redirect
		cmd_str.erase(cmd_str.begin() + gt_index, cmd_str.end());
	}

	if (lt_index > 0) //if an input redirect was provided
	{
		if (lt_index < cmd_str.size() - 2)
			throw runtime_error("Input redirection must only appear at end of command line.");
		else if (lt_index == cmd_str.size() - 1)
			throw runtime_error("No file provided for input redirection.");
			
		// get the PATH to search for redirect file
		vector<string>& path = mySet.get_path();
		
		// is the file in the current directory?
		in_fd = open( cmd_str[lt_index + 1].c_str(), O_RDONLY);
		
		// is the file in the PATH?
		if ( in_fd < 0)
		{
			for (vector<string>::const_iterator itr = path.begin(); itr != path.end(); ++itr)
			{
				string full_path = *itr + "/" + cmd_str[lt_index + 1];
				in_fd = open( full_path.c_str(), O_RDONLY );
				if (in_fd >= 0)
					break;
			}
			
			// the input redirect file could not be found
			if (in_fd < 0)
				throw runtime_error("Cannot find file for input redirection.");
		}
			
		// remove the tokens for input redirect
		cmd_str.erase(cmd_str.begin() + lt_index, cmd_str.end());
	}
	
	// parse commands
	vector<string>::const_iterator itr = cmd_str.begin();
	
	// for each command
	while (itr != cmd_str.end())
	{
		vector<string>::const_iterator jtr = itr;
		
		// create a new Command
		Command* c = new Command;
		
		// get the command name
		c->command_name = *jtr;
		
		// add the command name to command history, for command completion
		Command_Completion::add_command(*jtr);

		// move forward until a special character is reached
		// everything between the command name and this point
		// will be considered an argument
		while (jtr != cmd_str.end() &&
			   *jtr != "|" &&
			   *jtr != "<" &&
			   *jtr != ">" )
			jtr++;
		
		// get the argument count
		c->argc = jtr - itr;
		
		// create an argument array
		c->argv = new char*[c->argc + 1];
		
		// copy the arguments into the argument array
		int i = 0;
		for (; itr != jtr; ++itr)
		{
			c->argv[i] = new char[ itr->size() + 1 ];
			strcpy( c->argv[i], itr->c_str() );
			++i;
		}
		
		c->argv[i] = 0;
		
		// add the Command to the vector
		cmds.push_back(c);
		
		// move past the pipe
		while (jtr != cmd_str.end() && *jtr == "|")
			++jtr;
		
		itr = jtr;
	}
	
	// store the input and output redirect file descriptors for the first and last commands
	cmds[0]->in = in_fd;
	cmds[cmds.size()-1]->out = out_fd;
	
	return cmds;
}
